package com.nulldev.util.web.HttpServer3.extensions.http2;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.net.Socket;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import com.nulldev.util.JVM.JVM;
import com.nulldev.util.web.HttpServer3.extensions.http2.api_compat.HttpRequestDispatcher;
import com.nulldev.util.web.HttpServer3.extensions.http2.compat.HttpBodyStream;
import com.nulldev.util.web.HttpServer3.extensions.http2.compat.HttpRequest;
import com.nulldev.util.web.HttpServer3.extensions.http2.compat.HttpRequestValidator;
import com.nulldev.util.web.HttpServer3.extensions.http2.compat.IOContainer;
import com.nulldev.util.web.HttpServer3.extensions.http2.compat.ResponseBuilder;
import com.nulldev.util.web.HttpServer3.extensions.http2.errors.CannotParseRequestException;
import com.nulldev.util.web.HttpServer3.extensions.http2.hpack.HpackDecoder;
import com.nulldev.util.web.HttpServer3.extensions.http2.hpack.HpackEncoder;
import com.nulldev.util.web.HttpServer3.extensions.http2.types.GoAwayFrame;
import com.nulldev.util.web.HttpServer3.extensions.http2.types.PingFrame;
import com.nulldev.util.web.HttpServer3.extensions.http2.types.PriorityFrame;
import com.nulldev.util.web.HttpServer3.extensions.http2.types.SettingsFrame;
import com.nulldev.util.web.HttpServer3.extensions.http2.types.WindowUpdateFrame;
import com.nulldev.util.web.HttpServer3.extensions.http2.util.ServerThreadUtils;
import com.nulldev.util.web.HttpServer3.util.HttpServerUtil;

// TODO: stream dependency, errors, are still not implemented yet.
/**
 * The main connection for http2.
 */
public class Http2Connection {
	public static final int WINDOW_UPDATE_AMOUNT = 32768;

	private IOContainer ioContainer;
	private Socket connection;
	private StreamStore streamStore;
	private Set<Http2Stream> busyStreams;

	private HttpRequestValidator requestValidator;
	private HttpRequestDispatcher requestDispatcher;

	private Http2Configuration config;
	private Http2RequestParser http2Parser;
	private Http2FrameGenerator frameGenerator;

	private OutputStream os;
	private InputStream is;

	private boolean pingSent = false;

	public Http2Connection(IOContainer ioContainer, HttpRequestValidator requestValidator, HttpRequestDispatcher requestDispatcher) {
		this.ioContainer = ioContainer;
		this.connection = ioContainer.getSocket();
		this.streamStore = new StreamStore();
		busyStreams = new HashSet<>();
		this.requestValidator = requestValidator;
		this.requestDispatcher = requestDispatcher;
	}

	/**
	 * Setup http2 stuff.
	 */
	public void setup() {
		is = ioContainer.getInputStream();
		os = ioContainer.getOutputStream();
		config = new Http2Configuration();

		// "the request and response dynamic tables are separate."
		// https://tools.ietf.org/html/rfc7541#section-2.2
		HpackDecoder decoder = new HpackDecoder(new DynamicTable(config, StaticTable.instance.size() + 1));
		HpackEncoder encoder = new HpackEncoder(new DynamicTable(config, StaticTable.instance.size() + 1));

		frameGenerator = new Http2FrameGenerator(encoder, config);
		http2Parser = new Http2RequestParser(decoder, config);
	}

	/**
	 * @throws InterruptedException
	 * @see Http2Stream#processQueuedUpFrames()
	 */
	private void universalInputHandler(Http2Stream stream, Http2Frame frame) throws IOException, InvocationTargetException {
		if (HttpServerUtil.lowLevelDebugMode && !(frame.payload instanceof WindowUpdateFrame))
			HttpServerUtil.logger.debug("universalInputHandler: " + frame.toString());

		final Http2RequestConverter converter = stream.getConverter();

		if (converter.isHttpMessageFrame(frame)) {
			converter.addFrame(frame);

			final Optional<HttpRequest> optRequest = converter.toRequest();
			if (optRequest.isPresent()) {
				try (HttpRequest req = optRequest.get()) {
					req.setSocket(this.ioContainer);

					HttpServerUtil.logger.debug("req: " + req.toHttp2String());
					final ResponseBuilder response = handleHttpRequest(req);
					HttpServerUtil.logger.debug("resp: " + response);

					if (response.getHeaders().getHeader("X-Vws-Raw-Tunnel") != null) {
						try {
							ServerThreadUtils.socketTunnelInitialization(req, response, (remoteServerRes, os) -> {
								try {
									converter.streamResponseToStream(remoteServerRes, -1, stream);
								} catch (InvocationTargetException | IOException e) {
									throw new RuntimeException(e);
								}
							});
						} catch (InterruptedException e) {
							e.printStackTrace();
						}
					} else {
						converter.streamResponseToStream(response, -1, stream);
					}
				}
			}
		} else if (frame.payload instanceof SettingsFrame) {
			if ((frame.flags & SettingsFrame.ACK) == 0) {
				SettingsFrame settings = (SettingsFrame) frame.payload;
				config.apply(settings);
				stream.send(frameGenerator.settingsFrameAck(-1));
			}
		} else if (frame.payload instanceof WindowUpdateFrame) {
			stream.modifyServerWindow((WindowUpdateFrame) frame.payload);
		} else if (frame.payload instanceof PriorityFrame) {
			// Not implemented, very complex (involves stream dependency stuff)
		} else if (frame.payload instanceof PingFrame) {
			if ((frame.flags & PingFrame.ACK) == 0) {
				stream.send(frameGenerator.pingFrame(true));
			} else {
				pingReceived();
			}
		} else if (frame.payload instanceof GoAwayFrame) {
			connection.close();
		}
		stream.sendQueuedUpFrames();
	}

	/**
	 * @see Http2Stream#send()
	 */
	private void universalOutputHandler(Http2Stream stream, Http2Frame frame) throws IOException, InvocationTargetException {
		if (HttpServerUtil.lowLevelDebugMode && !(frame.payload instanceof WindowUpdateFrame))
			HttpServerUtil.logger.debug("Send: " + frame.toString());

		os.write(frame.toBytes());
	}

	/**
	 * Let the connection object to take over. The whole http2 communication starts
	 * here. This is a blocking operation until the connection is closed.
	 * <p>
	 * Make sure {@link #setup()} is already called.
	 */
	public void takeover() throws IOException, CannotParseRequestException, InvocationTargetException {
		streamStore.addStream(new Http2Stream(0, this::universalInputHandler, this::universalOutputHandler, frameGenerator));
		streamStore.get(0).send(frameGenerator.settingsFrame(0));
		initConnectionChecker();

		final ExecutorService executorService = Executors.newFixedThreadPool(4);
		while (isConnected()) {
			try {
				Http2Frame requestFrame = http2Parser.parse(is);
				Http2Stream stream = findCorrespondingStream(requestFrame);
				stream.queueUpClientFrames(requestFrame);

				// TODO: Bug fixed regarding race condition, but still need more testing
				if (!busyStreams.contains(stream)) {
					busyStreams.add(stream);
					executorService.submit(() -> {
						try {
							stream.processQueuedUpFrames();
							busyStreams.remove(stream);
						} catch (Exception e) {
							JVM.errorln(e);
						}
					});
				}
			} catch (Exception ex) {
				JVM.errorln(ex);
				break;
			}
		}
		executorService.shutdown();
	}

	private Http2Stream findCorrespondingStream(Http2Frame requestFrame) throws InvocationTargetException, IOException {
		Optional<Http2Stream> optStream = streamStore.findStream(requestFrame);
		Http2Stream stream;
		if (optStream.isPresent()) {
			stream = optStream.get();
		} else {
			stream = new Http2Stream(requestFrame.streamIdentifier, this::universalInputHandler, this::universalOutputHandler, frameGenerator);
			streamStore.addStream(stream);
		}
		return stream;
	}

	private ResponseBuilder handleHttpRequest(HttpRequest request) throws InvocationTargetException {
		if (!requestValidator.requestIsValid(request)) {
			HttpServerUtil.logger.debug("handleHttpRequest: dropping invalid request...");
			request.invalid();
			final ResponseBuilder response = ResponseBuilder.getInstance(400, new HttpBodyStream());
			return response;
		} else if (request.isValid()) {
			HttpServerUtil.logger.debug("handleHttpRequest: processing...");
			return requestDispatcher.dispatchObjectToHandlers(request);
		} else {
			HttpServerUtil.logger.debug("handleHttpRequest: bad request.");
			final ResponseBuilder response = ResponseBuilder.getInstance(400, new HttpBodyStream());
			return response;
		}
	}

	public Socket getConnection() {
		return connection;
	}

	public StreamStore getStreams() {
		return streamStore;
	}

	public boolean isConnected() {
		// Socket conn = connection.getUnderlyingSocket();
		final Socket conn = connection;
		return !(conn.isInputShutdown() || conn.isOutputShutdown());
	}

	private Thread initConnectionChecker() {
		final Thread connectionChecker = new Thread("HTTP2_CONN_CHECK") {
			public void run() {
				try {
					Socket socket = ioContainer.getSocket();
					while (!socket.isClosed()) {
						Thread.sleep(HttpServerUtil.WEBSOCKET_PING_INTERVAL_MILSEC);
						if (pingSent)
							socket.close();
						sendPing();
					}
				} catch (Exception e) {
					e.printStackTrace();
				}
				return;
			}
		};
		connectionChecker.setDaemon(true);
		connectionChecker.start();
		return connectionChecker;
	}

	private void sendPing() throws IOException, InvocationTargetException {
		universalOutputHandler(streamStore.findStream(0).get(), frameGenerator.pingFrame(false));
		pingSent = true;
	}

	private void pingReceived() {
		pingSent = false;
	}
}
